PDK#

gdsfactory includes a generic PDK, that you can use as an inspiration to create your own pdks.

The PDK allows you to register:

  • cell functions that return Components from a ComponentSpec (string, Component, ComponentFactory or dict). Also known as PCells (parametric cells).

  • cross_section functions that return CrossSection from a CrossSection Spec (string, CrossSection, CrossSectionFactory or dict).

  • layers that return a GDS Layer from a string, an int or a Tuple[int, int].

You can only have one active PDK at a time. Thanks to PDK you can access components, cross_sections or layers using a string.

Depending on the active pdk:

  • get_layer returns a Layer from the registered layers.

  • get_component returns a Component from the registered cells or containers.

  • get_cross_section returns a CrossSection from the registered cross_sections.

layers#

GDS layers are a tuple of two integer number gdslayer/gdspurpose

You can define all the layers from your PDK:

  1. From a Klayout lyp (layer properties file).

  2. From scratch, adding all your layers into a class.

Lets generate the layers definition code from a klayout lyp file.

[1]:
import gdsfactory as gf
from gdsfactory.layers import lyp_to_dataclass
from gdsfactory.config import PATH

print(lyp_to_dataclass(PATH.klayout_lyp))
2022-11-07 00:09:30.894 | INFO     | gdsfactory.config:<module>:45 - Load '/home/runner/work/gdsfactory/gdsfactory/gdsfactory' 5.54.0

from pydantic import BaseModel
from gdsfactory.types import Layer


class LayerMap(BaseModel):
    CAPACITOR: Layer = (42, 0)
    DEEPETCH: Layer = (3, 6)
    DEEPTRENCH: Layer = (4, 0)
    DICING: Layer = (65, 0)
    DRC_EXCLUDE: Layer = (67, 0)
    DRC_MARKER: Layer = (205, 0)
    DevRec: Layer = (68, 0)
    ERROR_MARKER: Layer = (207, 0)
    Errors: Layer = (69, 0)
    FLOORPLAN: Layer = (64, 0)
    FbrTgt: Layer = (81, 0)
    GE: Layer = (5, 0)
    GENPP: Layer = (26, 0)
    GEPPP: Layer = (29, 0)
    LABEL: Layer = (201, 0)
    LABEL_INSTANCES: Layer = (206, 0)
    LABEL_SETTINGS: Layer = (202, 0)
    Lumerical: Layer = (733, 0)
    M1: Layer = (41, 0)
    M1TILES: Layer = (191, 0)
    M2: Layer = (45, 0)
    M3: Layer = (49, 0)
    METALOPEN: Layer = (46, 0)
    MH: Layer = (47, 0)
    MONITOR: Layer = (101, 0)
    N: Layer = (20, 0)
    NOTILE_M1: Layer = (71, 0)
    NOTILE_M2: Layer = (72, 0)
    NOTILE_M3: Layer = (73, 0)
    NP: Layer = (22, 0)
    NPP: Layer = (24, 0)
    OXIDE_ETCH: Layer = (6, 0)
    P: Layer = (21, 0)
    PDPP: Layer = (27, 0)
    PP: Layer = (23, 0)
    PPP: Layer = (25, 0)
    PinRec: Layer = (1, 10)
    PinRecM: Layer = (1, 11)
    SHALLOWETCH: Layer = (2, 6)
    SHOW_PORTS: Layer = (1, 12)
    SILICIDE: Layer = (39, 0)
    SIM_REGION: Layer = (100, 0)
    SITILES: Layer = (190, 0)
    SLAB150: Layer = (2, 0)
    SLAB150CLAD: Layer = (2, 9)
    SLAB90: Layer = (3, 0)
    SLAB90CLAD: Layer = (3, 1)
    SOURCE: Layer = (110, 0)
    TE: Layer = (203, 0)
    TM: Layer = (204, 0)
    Text: Layer = (66, 0)
    VIA1: Layer = (44, 0)
    VIA2: Layer = (43, 0)
    VIAC: Layer = (40, 0)
    WGCLAD: Layer = (111, 0)
    WGN: Layer = (34, 0)
    WGNCLAD: Layer = (36, 0)
    Waveguide: Layer = (1, 0)
    XS_BOX: Layer = (300, 0)
    XS_GE: Layer = (315, 0)
    XS_M1: Layer = (304, 0)
    XS_M2: Layer = (399, 0)
    XS_MH: Layer = (306, 0)
    XS_N: Layer = (320, 0)
    XS_NPP: Layer = (321, 0)
    XS_OVERLAY: Layer = (311, 0)
    XS_OXIDE_M1: Layer = (305, 0)
    XS_OXIDE_M2: Layer = (307, 0)
    XS_OXIDE_M3: Layer = (311, 0)
    XS_OXIDE_MH: Layer = (317, 0)
    XS_OXIDE_ML: Layer = (309, 0)
    XS_OX_SI: Layer = (302, 0)
    XS_P: Layer = (330, 0)
    XS_PDPP: Layer = (327, 0)
    XS_PPP: Layer = (331, 0)
    XS_SI: Layer = (301, 0)
    XS_SIN: Layer = (319, 0)
    XS_SIN2: Layer = (305, 0)
    XS_SI_SLAB: Layer = (313, 0)
    XS_VIA1: Layer = (308, 0)
    XS_VIA2: Layer = (310, 0)
    XS_VIAC: Layer = (303, 0)

    class Config:
        frozen = True
        extra = "forbid"


LAYER = LayerMap()

[2]:
from pydantic import BaseModel
from gdsfactory.types import Layer


class LayerMap(BaseModel):
    WG: Layer = (1, 0)
    DEVREC: Layer = (68, 0)
    PORT: Layer = (1, 10)
    PORTE: Layer = (1, 11)
    LABEL: Layer = (201, 0)
    LABEL_INSTANCES: Layer = (206, 0)
    LABEL_SETTINGS: Layer = (202, 0)
    LUMERICAL: Layer = (733, 0)
    M1: Layer = (41, 0)
    M2: Layer = (45, 0)
    M3: Layer = (49, 0)
    N: Layer = (20, 0)
    NP: Layer = (22, 0)
    NPP: Layer = (24, 0)
    OXIDE_ETCH: Layer = (6, 0)
    P: Layer = (21, 0)
    PDPP: Layer = (27, 0)
    PP: Layer = (23, 0)
    PPP: Layer = (25, 0)
    PinRec: Layer = (1, 10)
    PinRecM: Layer = (1, 11)
    SHALLOWETCH: Layer = (2, 6)
    SILICIDE: Layer = (39, 0)
    SIM_REGION: Layer = (100, 0)
    SITILES: Layer = (190, 0)
    SLAB150: Layer = (2, 0)
    SLAB150CLAD: Layer = (2, 9)
    SLAB90: Layer = (3, 0)
    SLAB90CLAD: Layer = (3, 1)
    SOURCE: Layer = (110, 0)
    TE: Layer = (203, 0)
    TEXT: Layer = (66, 0)
    TM: Layer = (204, 0)
    Text: Layer = (66, 0)
    VIA1: Layer = (44, 0)
    VIA2: Layer = (43, 0)
    VIAC: Layer = (40, 0)
    WGCLAD: Layer = (111, 0)
    WGN: Layer = (34, 0)
    WGNCLAD: Layer = (36, 0)

    class Config:
        frozen = True
        extra = "forbid"


LAYER = LayerMap()

There are some default layers in some generic components and cross_sections, that it may be convenient adding.

Layer

Purpose

DEVREC

device recognition layer. For connectivity checks.

PORT

optical port pins. For connectivity checks.

PORTE

electrical port pins. For connectivity checks.

SHOW_PORTS

add port pin markers when Component.show(show_ports=True)

LABEL

for adding labels to grating couplers for automatic testing.

LABEL_INSTANCE

for adding instance labels on gf.read.from_yaml

TE

for TE polarization fiber marker.

TM

for TM polarization fiber marker.

class LayersConvenient(BaseModel):
    DEVREC: Layer = (68, 0)
    PORT: Layer = (1, 10)  # PinRec optical
    PORTE: Layer = (1, 11)  # PinRec electrical
    SHOW_PORTS: Layer = (1, 12)

    LABEL: Layer = (201, 0)
    LABEL_INSTANCE: Layer = (66, 0)
    TE: Layer = (203, 0)
    TM: Layer = (204, 0)

cross_sections#

You can create a CrossSection from scratch or you can customize the cross_section functions in gf.cross_section

[3]:
import gdsfactory as gf

strip2 = gf.partial(gf.cross_section.strip, layer=(2, 0))
[4]:
c = gf.components.straight(cross_section=strip2)
c
../_images/notebooks_08_pdk_7_0.png
[4]:
straight_bf73fda8: uid 4673455e, ports ['o1', 'o2'], references [], 4 polygons
[5]:
import gdsfactory as gf

pin = gf.partial(
    gf.cross_section.strip,
    sections=(
        gf.tech.Section(width=2, layer=gf.LAYER.N, offset=+1),
        gf.tech.Section(width=2, layer=gf.LAYER.P, offset=-1),
    ),
)
c = gf.components.straight(cross_section=pin)
c
../_images/notebooks_08_pdk_8_0.png
[5]:
straight_0cfc69af: uid dc982edf, ports ['o1', 'o2'], references [], 6 polygons
[6]:
strip_wide = gf.partial(gf.cross_section.strip, width=3)
[7]:
strip = gf.partial(
    gf.cross_section.strip, auto_widen=True
)  # auto_widen tapers to wider waveguides for lower loss in long straight sections.
[8]:
cross_sections = dict(strip_wide=strip_wide, pin=pin, strip=strip)

cells#

Cells are functions that return Components. They are parametrized and accept also cells as parameters, so you can build many levels of complexity. Cells are also known as PCells or parametric cells.

You can customize the function default arguments easily thanks to functools.partial Lets customize the default arguments of a library of cells.

For example, you can make some wide MMIs for a particular technology. Lets say the best MMI width you found it to be 9um.

[9]:
import gdsfactory as gf

mmi1x2 = gf.partial(gf.components.mmi1x2, width_mmi=9)
mmi2x2 = gf.partial(gf.components.mmi2x2, width_mmi=9)

cells = dict(mmi1x2=mmi1x2, mmi2x2=mmi2x2)

PDK#

You can register Layers, ComponentFactories (Parametric cells) and CrossSectionFactories (cross_sections) into a PDK. Then you can access them by a string after you activate the pdk PDK.activate().

LayerSpec#

You can access layers from the active Pdk using the layer name or a tuple/list of two numbers.

[10]:
pdk1 = gf.Pdk(
    name="fab1",
    layers=LAYER.dict(),
    cross_sections=cross_sections,
    cells=cells,
    base_pdk=gf.pdk.GENERIC,
    sparameters_path=gf.config.sparameters_path,
    layer_colors=gf.layers.LAYER_COLORS,
)
pdk1.activate()
[11]:
pdk1.get_layer("WG")
[11]:
(1, 0)
[12]:
pdk1.get_layer([1, 0])
[12]:
[1, 0]

CrossSectionSpec#

You can access cross_sections from the pdk from the cross_section name, or using a dict to customize the CrossSection

[13]:
pdk1.get_cross_section("pin")
[13]:
CrossSection(layer='WG', width=0.5, offset=0.0, radius=10.0, width_wide=None, auto_widen=False, auto_widen_minimum_length=200.0, taper_length=10.0, bbox_layers=[], bbox_offsets=[], cladding_layers=['DEVREC'], cladding_offsets=(0.0,), sections=[Section(width=2.0, offset=1.0, layer=(20, 0), port_names=(None, None), port_types=('optical', 'optical'), name=None, hidden=False), Section(width=2.0, offset=-1.0, layer=(21, 0), port_names=(None, None), port_types=('optical', 'optical'), name=None, hidden=False)], port_names=('o1', 'o2'), port_types=('optical', 'optical'), min_length=0.01, start_straight_length=0.01, end_straight_length=0.01, snap_to_grid=None, decorator=None, add_pins=functools.partial(<function add_pins_siepic at 0x7fb3009d0e50>, pin_length=0.002), add_bbox=<function add_bbox_siepic at 0x7fb3009d0ee0>, info={}, name=None, add_center_section=True)
[14]:
cross_section_spec_string = "pin"
gf.components.straight(cross_section=cross_section_spec_string)
../_images/notebooks_08_pdk_20_0.png
[14]:
straight_cross_sectionpin: uid bd94bbde, ports ['o1', 'o2'], references [], 6 polygons
[15]:
cross_section_spec_dict = dict(cross_section="pin", settings=dict(width=2))
print(pdk1.get_cross_section(cross_section_spec_dict))
wg_pin = gf.components.straight(cross_section=cross_section_spec_dict)
wg_pin
layer='WG' width=2.0 offset=0.0 radius=10.0 width_wide=None auto_widen=False auto_widen_minimum_length=200.0 taper_length=10.0 bbox_layers=[] bbox_offsets=[] cladding_layers=['DEVREC'] cladding_offsets=(0.0,) sections=[Section(width=2.0, offset=1.0, layer=(20, 0), port_names=(None, None), port_types=('optical', 'optical'), name=None, hidden=False), Section(width=2.0, offset=-1.0, layer=(21, 0), port_names=(None, None), port_types=('optical', 'optical'), name=None, hidden=False)] port_names=('o1', 'o2') port_types=('optical', 'optical') min_length=0.01 start_straight_length=0.01 end_straight_length=0.01 snap_to_grid=None decorator=None add_pins=functools.partial(<function add_pins_siepic at 0x7fb3009d0e50>, pin_length=0.002) add_bbox=<function add_bbox_siepic at 0x7fb3009d0ee0> info={} name=None add_center_section=True
../_images/notebooks_08_pdk_21_1.png
[15]:
straight_361ab848: uid 0c6448d7, ports ['o1', 'o2'], references [], 6 polygons

ComponentSpec#

You can get Component from the pdk using the cell name (string) or a dict.

[16]:
pdk1.get_component("mmi1x2")
../_images/notebooks_08_pdk_23_0.png
[16]:
mmi1x2_width_mmi9: uid 8b91b0e8, ports ['o1', 'o2', 'o3'], references [], 19 polygons
[17]:
pdk1.get_component(dict(component="mmi1x2", settings=dict(length_mmi=10)))
../_images/notebooks_08_pdk_24_0.png
[17]:
mmi1x2_length_mmi10_width_mmi9: uid b9b098ff, ports ['o1', 'o2', 'o3'], references [], 19 polygons

Now you can define PDKs for different Fabs

FabA#

FabA only has one waveguide layer available that is defined in GDS layer (30, 0)

The waveguide traces are 2um wide.

[18]:
import gdsfactory as gf
from gdsfactory.types import Layer, LayerColors, LayerColor, LayerStack, LayerLevel
from pydantic import BaseModel

nm = 1e-3


class LayerMap(BaseModel):
    WG: Layer = (34, 0)
    SLAB150: Layer = (2, 0)
    DEVREC: Layer = (68, 0)
    PORT: Layer = (1, 10)
    PORTE: Layer = (1, 11)
    TE: Layer = (203, 0)
    TM: Layer = (204, 0)
    TEXT: Layer = (66, 0)


LAYER = LayerMap()

layer_colors = dict(
    WG=LayerColor(gds_layer=34, gds_datatype=0, color="gold"),
    SLAB150=LayerColor(gds_layer=2, gds_datatype=0, color="red"),
    TE=LayerColor(gds_layer=203, gds_datatype=0, color="green"),
)
LAYER_COLORS = LayerColors(layers=layer_colors)


def get_layer_stack_faba(
    thickness_wg: float = 500 * nm, thickness_slab: float = 150 * nm
) -> LayerStack:
    """Returns fabA LayerStack"""
    ## TODO: Translate xml in lumerical process file
    return LayerStack(
        layers=dict(
            strip=LayerLevel(
                layer=LAYER.WG,
                thickness=thickness_wg,
                zmin=0.0,
                material="si",
            ),
            strip2=LayerLevel(
                layer=LAYER.SLAB150,
                thickness=thickness_slab,
                zmin=0.0,
                material="si",
            ),
        )
    )


LAYER_STACK = get_layer_stack_faba()

WIDTH = 2

# Specify a cross_section to use
strip = gf.partial(gf.cross_section.cross_section, width=WIDTH, layer=LAYER.WG)

mmi1x2 = gf.partial(
    gf.components.mmi1x2,
    width=WIDTH,
    width_taper=WIDTH,
    width_mmi=3 * WIDTH,
    cross_section=strip,
)

generic_pdk = gf.pdk.GENERIC

fab_a = gf.Pdk(
    name="Fab_A",
    cells=dict(mmi1x2=mmi1x2),
    cross_sections=dict(strip=strip),
    layers=LAYER.dict(),
    base_pdk=generic_pdk,
    sparameters_path=gf.config.sparameters_path,
    layer_colors=LAYER_COLORS,
    layer_stack=LAYER_STACK,
)
fab_a.activate()

gc = gf.partial(
    gf.components.grating_coupler_elliptical_te, layer=LAYER.WG, cross_section=strip
)

c = gf.components.mzi()
c_gc = gf.routing.add_fiber_array(component=c, grating_coupler=gc, with_loopback=False)
c_gc
../_images/notebooks_08_pdk_26_0.png
[18]:
mzi_add_fiber_array_3e89adf8: uid 6a8c960e, ports ['vertical_te_00', 'vertical_te_01'], references ['bend_euler_1', 'straight_1', 'straight_2', 'bend_euler_2', 'straight_3', 'straight_4', 'grating_coupler_elliptical_1', 'grating_coupler_elliptical_2', 'mzi_1'], 0 polygons
[19]:
c = c_gc.to_3d()
c.show(show_ports=True)
[19]:

FabB#

FabB has photonic waveguides that require rectangular cladding layers to avoid dopants

Lets say that the waveguides are defined in layer (2, 0) and are 0.3um wide, 1um thick

[20]:
import gdsfactory as gf
from gdsfactory.types import Layer, LayerColor, LayerColors, LayerStack, LayerLevel
from pydantic import BaseModel

nm = 1e-3


class LayerMap(BaseModel):
    WG: Layer = (2, 0)
    SLAB150: Layer = (3, 0)
    DEVREC: Layer = (68, 0)
    PORT: Layer = (1, 10)
    PORTE: Layer = (1, 11)
    TE: Layer = (203, 0)
    TM: Layer = (204, 0)
    TEXT: Layer = (66, 0)
    LABEL: Layer = (201, 0)
    DOPING_BLOCK1: Layer = (61, 0)
    DOPING_BLOCK2: Layer = (62, 0)


LAYER = LayerMap()

layer_colors = dict(
    WG=LayerColor(gds_layer=2, gds_datatype=0, color="red"),
    SLAB150=LayerColor(gds_layer=3, gds_datatype=0, color="blue"),
    TE=LayerColor(gds_layer=203, gds_datatype=0, color="green"),
    PORT=LayerColor(gds_layer=1, gds_datatype=10, color="green", alpha=0),
    DOPING_BLOCK1=LayerColor(gds_layer=61, gds_datatype=0, color="green", alpha=0),
    DOPING_BLOCK2=LayerColor(gds_layer=62, gds_datatype=0, color="green", alpha=0),
)
LAYER_COLORS = LayerColors(layers=layer_colors)


def get_layer_stack_fab_b(
    thickness_wg: float = 1000 * nm, thickness_slab: float = 150 * nm
) -> LayerStack:
    """Returns fabA LayerStack"""
    ## TODO: Translate xml in lumerical process file
    return LayerStack(
        layers=dict(
            strip=LayerLevel(
                layer=LAYER.WG,
                thickness=thickness_wg,
                zmin=0.0,
                material="si",
            ),
            strip2=LayerLevel(
                layer=LAYER.SLAB150,
                thickness=thickness_slab,
                zmin=0.0,
                material="si",
            ),
        )
    )


LAYER_STACK = get_layer_stack_fab_b()


WIDTH = 0.3
BBOX_LAYERS = (LAYER.DOPING_BLOCK1, LAYER.DOPING_BLOCK2)
BBOX_OFFSETS = (3, 3)

# use cladding_layers and cladding_offsets if the foundry prefers conformal blocking doping layers instead of squared
# bbox_layers and bbox_offsets makes rectangular waveguides.
strip = gf.partial(
    gf.cross_section.cross_section,
    width=WIDTH,
    layer=LAYER.WG,
    # bbox_layers=BBOX_LAYERS,
    # bbox_offsets=BBOX_OFFSETS,
    cladding_layers=BBOX_LAYERS,
    cladding_offsets=BBOX_OFFSETS,
)

straight = gf.partial(gf.components.straight, cross_section=strip)
bend_euler = gf.partial(gf.components.bend_euler, cross_section=strip)
mmi1x2 = gf.partial(
    gf.components.mmi1x2,
    cross_section=strip,
    width=WIDTH,
    width_taper=WIDTH,
    width_mmi=4 * WIDTH,
)
mzi = gf.partial(gf.components.mzi, cross_section=strip, splitter=mmi1x2)
gc = gf.partial(
    gf.components.grating_coupler_elliptical_te, layer=LAYER.WG, cross_section=strip
)

cells = dict(
    gc=gc,
    mzi=mzi,
    mmi1x2=mmi1x2,
    bend_euler=bend_euler,
    straight=straight,
    taper=gf.components.taper,
)
cross_sections = dict(strip=strip)

pdk = gf.Pdk(
    name="fab_b",
    cells=cells,
    cross_sections=cross_sections,
    layers=LAYER.dict(),
    sparameters_path=gf.config.sparameters_path,
    layer_colors=LAYER_COLORS,
    layer_stack=LAYER_STACK,
)
pdk.activate()


c = mzi()
wg_gc = gf.routing.add_fiber_array(
    component=c, grating_coupler=gc, cross_section=strip, with_loopback=False
)
wg_gc
../_images/notebooks_08_pdk_29_0.png
[20]:
mzi_a1b57e95_add_fiber__53b5841b: uid 06c2002a, ports ['vertical_te_00', 'vertical_te_01'], references ['bend_euler_1', 'straight_1', 'straight_2', 'bend_euler_2', 'straight_3', 'straight_4', 'grating_coupler_elliptical_1', 'grating_coupler_elliptical_2', 'mzi_1'], 0 polygons
[21]:
c = wg_gc.to_3d()
c.show(show_ports=True)
[21]:

FabC#

Lets assume that fab C has similar technology to the generic PDK in gdsfactory and that you just want to remap some layers, and adjust the widths.

[22]:
from typing import Callable, Dict, Optional, Tuple
from pydantic import BaseModel

import gdsfactory as gf
from gdsfactory.add_pins import add_pin_rectangle_inside
from gdsfactory.component import Component
from gdsfactory.cross_section import cross_section
from gdsfactory.tech import LayerLevel, LayerStack
from gdsfactory.types import Layer, LayerColor, LayerColors


nm = 1e-3


class LayerMap(BaseModel):
    WG: Layer = (10, 1)
    WG_CLAD: Layer = (10, 2)
    WGN: Layer = (34, 0)
    WGN_CLAD: Layer = (36, 0)
    SLAB150: Layer = (2, 0)
    DEVREC: Layer = (68, 0)
    PORT: Layer = (1, 10)
    PORTE: Layer = (1, 11)
    TE: Layer = (203, 0)
    TM: Layer = (204, 0)
    TEXT: Layer = (66, 0)
    LABEL: Layer = (201, 0)


LAYER = LayerMap()
WIDTH_NITRIDE_OBAND = 0.9
WIDTH_NITRIDE_CBAND = 1.0
PORT_TYPE_TO_LAYER = dict(optical=(100, 0))

# This is something you usually define in klayout
layer_colors = dict(
    WG=LayerColor(gds_layer=10, gds_datatype=1, color="black"),
    SLAB150=LayerColor(gds_layer=2, gds_datatype=0, color="blue"),
    WGN=LayerColor(gds_layer=34, gds_datatype=0, color="orange"),
    WGN_CLAD=LayerColor(gds_layer=36, gds_datatype=0, color="blue", alpha=0),
    TE=LayerColor(gds_layer=203, gds_datatype=0, color="green"),
    PORT=LayerColor(gds_layer=1, gds_datatype=10, color="green", alpha=0),
    DOPING_BLOCK1=LayerColor(gds_layer=61, gds_datatype=0, color="green", alpha=0),
    DOPING_BLOCK2=LayerColor(gds_layer=62, gds_datatype=0, color="green", alpha=0),
)
LAYER_COLORS = LayerColors(layers=layer_colors)


def get_layer_stack_fab_c(
    thickness_wg: float = 350.0 * nm, thickness_clad: float = 3.0
) -> LayerStack:
    """Returns generic LayerStack"""
    return LayerStack(
        layers=dict(
            core=LayerLevel(
                layer=LAYER.WGN,
                thickness=thickness_wg,
                zmin=0,
            ),
            clad=LayerLevel(
                layer=LAYER.WGN_CLAD,
                thickness=0,
                zmin=0,
            ),
        )
    )


LAYER_STACK = get_layer_stack_fab_c()


def add_pins(
    component: Component,
    function: Callable = add_pin_rectangle_inside,
    pin_length: float = 0.5,
    port_layer: Layer = LAYER.PORT,
    **kwargs,
) -> Component:
    """Add Pin port markers.

    Args:
        component: to add ports.
        function:
        pin_length:
        port_layer:
        function: kwargs.

    """
    for p in component.ports.values():
        function(
            component=component,
            port=p,
            layer=port_layer,
            layer_label=port_layer,
            pin_length=pin_length,
            **kwargs,
        )
    return component


# cross_sections

bbox_layers = [LAYER.WGN_CLAD]
bbox_offsets = [3]


# Nitride Cband
xs_nc = gf.partial(
    cross_section,
    width=WIDTH_NITRIDE_CBAND,
    layer=LAYER.WGN,
    bbox_layers=bbox_layers,
    bbox_offsets=bbox_offsets,
    add_pins=add_pins,
)
# Nitride Oband
xs_no = gf.partial(
    cross_section,
    width=WIDTH_NITRIDE_OBAND,
    layer=LAYER.WGN,
    bbox_layers=bbox_layers,
    bbox_offsets=bbox_offsets,
    add_pins=add_pins,
)


cross_sections = dict(xs_nc=xs_nc, xs_no=xs_no, strip=xs_nc)

# LEAF cells have pins
mmi1x2_nc = gf.partial(
    gf.components.mmi1x2,
    width=WIDTH_NITRIDE_CBAND,
    cross_section=xs_nc,
)
mmi1x2_no = gf.partial(
    gf.components.mmi1x2,
    width=WIDTH_NITRIDE_OBAND,
    cross_section=xs_no,
)
bend_euler_nc = gf.partial(
    gf.components.bend_euler,
    cross_section=xs_nc,
)
straight_nc = gf.partial(
    gf.components.straight,
    cross_section=xs_nc,
)
bend_euler_no = gf.partial(
    gf.components.bend_euler,
    cross_section=xs_no,
)
straight_no = gf.partial(
    gf.components.straight,
    cross_section=xs_no,
)

gc_nc = gf.partial(
    gf.components.grating_coupler_elliptical_te,
    grating_line_width=0.6,
    layer=LAYER.WGN,
    cross_section=xs_nc,
)

# HIERARCHICAL cells are made of leaf cells
mzi_nc = gf.partial(
    gf.components.mzi,
    cross_section=xs_nc,
    splitter=mmi1x2_nc,
    straight=straight_nc,
    bend=bend_euler_nc,
)
mzi_no = gf.partial(
    gf.components.mzi,
    cross_section=xs_no,
    splitter=mmi1x2_no,
    straight=straight_no,
    bend=bend_euler_no,
)


cells = dict(
    mmi1x2_nc=mmi1x2_nc,
    mmi1x2_no=mmi1x2_no,
    bend_euler_nc=bend_euler_nc,
    bend_euler_no=bend_euler_no,
    straight_nc=straight_nc,
    straight_no=straight_no,
    gc_nc=gc_nc,
    mzi_nc=mzi_nc,
    mzi_no=mzi_no,
)

pdk = gf.Pdk(
    name="fab_c",
    cells=cells,
    cross_sections=cross_sections,
    layers=LAYER.dict(),
    sparameters_path=gf.config.sparameters_path,
    layer_colors=LAYER_COLORS,
    layer_stack=LAYER_STACK,
)
pdk.activate()
[23]:
mzi = mzi_nc()
mzi_gc = gf.routing.add_fiber_single(
    component=mzi,
    grating_coupler=gc_nc,
    cross_section=xs_nc,
    optical_routing_type=1,
    straight=straight_nc,
    bend=bend_euler_nc,
)
mzi_gc
../_images/notebooks_08_pdk_33_0.png
[23]:
mzi_6feba2af_move_94728_862eed06: uid d2b59bb5, ports ['vertical_te_00', 'vertical_te_1', 'loopback1', 'loopback2'], references ['move_1', 'straight_1', 'straight_2', 'grating_coupler_elliptical_1', 'grating_coupler_elliptical_2', 'straight_3', 'grating_coupler_elliptical_3', 'grating_coupler_elliptical_4'], 0 polygons
[24]:
c = mzi_gc.to_3d()
c.show(show_ports=True)
[24]:
[25]:
ls = get_layer_stack_fab_c()

PDK Testing#

To make sure all your PDK Pcells produce the components that you want, it’s important to test your PDK cells.

As you write your own cell functions you want to make sure you do not break or produced unwanted regressions later on. You should write tests for this.

Make sure you create a test_components.py file for pytest to test your PDK.

[26]:
"""This code tests all your cells in the PDK

it will test 3 things:

1. difftest: will test the GDS geometry of a new GDS compared to a reference. Thanks to Klayout fast booleans.add()
2. settings test: will compare the settings in YAML with a reference YAML file.add()
3. ensure ports are on grid, to avoid port snapping errors that can create 1nm gaps later on when you build circuits.

"""

import pathlib

import pytest
from pytest_regressions.data_regression import DataRegressionFixture

import gdsfactory as gf
from gdsfactory.difftest import difftest

try:
    dirpath = pathlib.Path(__file__).absolute().with_suffix(".gds")
except Exception:
    dirpath = pathlib.Path.cwd()


component_names = list(pdk.cells.keys())
factory = pdk.cells


@pytest.fixture(params=component_names, scope="function")
def component_name(request) -> str:
    return request.param


def test_gds(component_name: str) -> None:
    """Avoid regressions in GDS names, shapes and layers.
    Runs XOR and computes the area."""
    component = factory[component_name]()
    test_name = f"fabc_{component_name}"
    difftest(component, test_name=test_name, dirpath=dirpath)


def test_settings(component_name: str, data_regression: DataRegressionFixture) -> None:
    """Avoid regressions in component settings and ports."""
    component = factory[component_name]()
    data_regression.check(component.to_dict())


def test_assert_ports_on_grid(component_name: str):
    """Ensures all ports are on grid to avoid 1nm gaps"""
    component = factory[component_name]()
    component.assert_ports_on_grid()

PDK decorator#

You can also define a PDK decorator (function) that runs over every PDK PCell.

[27]:
import gdsfactory as gf
from gdsfactory.pdk import GENERIC
from gdsfactory.decorators import flatten_invalid_refs, has_valid_transformations

GENERIC.activate()


@gf.cell
def _demo_non_manhattan():
    c = gf.Component("bend")
    b = c << gf.components.bend_circular(angle=30)
    s = c << gf.components.straight(length=5)
    s.connect("o1", b.ports["o2"])
    return c


c1 = _demo_non_manhattan()
print(has_valid_transformations(c1))
c1
False
../_images/notebooks_08_pdk_39_1.png
[27]:
_demo_non_manhattan: uid 138ee2dd, ports [], references ['bend_circular_1', 'straight_1'], 0 polygons

if you zoom in between the bends you will see a notch between waveguides due to non-manhattan connection between the bends.

gap

You an fix it with the flatten_invalid_refs decorator.

[28]:
c2 = _demo_non_manhattan(decorator=flatten_invalid_refs)
print(has_valid_transformations(c2))
c2
True
../_images/notebooks_08_pdk_41_1.png
[28]:
_demo_non_manhattan_60a273d3: uid c90eff17, ports [], references ['bend_circular_1', 'straight_1'], 0 polygons

If you zoom in the connection the decorator you can see it fixed the issue.

no gap

Version control components#

For version control your component library you can use GIT

For tracking changes you can add Component changelog in the Pcell docstring.

[29]:
from typing import Tuple

import gdsfactory as gf
from gdsfactory.types import LayerSpec


@gf.cell
def litho_ruler(
    height: float = 2,
    width: float = 0.5,
    spacing: float = 2.0,
    scale: Tuple[float, ...] = (3, 1, 1, 1, 1, 2, 1, 1, 1, 1),
    num_marks: int = 21,
    layer: LayerSpec = "WG",
) -> gf.Component:
    """Ruler structure for lithographic measurement.

    Includes marks of varying scales to allow for easy reading by eye.

    based on phidl.geometry

    Args:
        height: Height of the ruling marks in um.
        width: Width of the ruling marks in um.
        spacing: Center-to-center spacing of the ruling marks in um.
        scale: Height scale pattern of marks.
        num_marks: Total number of marks to generate.
        layer: Specific layer to put the ruler geometry on.
    """
    D = gf.Component()
    for n in range(num_marks):
        h = height * scale[n % len(scale)]
        D << gf.components.rectangle(size=(width, h), layer=layer)

    D.distribute(direction="x", spacing=spacing, separation=False, edge="x")
    D.align(alignment="ymin")
    D.flatten()
    return D


c = litho_ruler()
c
../_images/notebooks_08_pdk_44_0.png
[29]:
litho_ruler: uid 5dbc12ca, ports [], references ['rectangle_1', 'rectangle_2', 'rectangle_3', 'rectangle_4', 'rectangle_5', 'rectangle_6', 'rectangle_7', 'rectangle_8', 'rectangle_9', 'rectangle_10', 'rectangle_11', 'rectangle_12', 'rectangle_13', 'rectangle_14', 'rectangle_15', 'rectangle_16', 'rectangle_17', 'rectangle_18', 'rectangle_19', 'rectangle_20', 'rectangle_21'], 0 polygons

Lets assume that later on you change the code inside the Pcell and want to keep a changelog.

[30]:
@gf.cell
def litho_ruler(
    height: float = 2,
    width: float = 0.5,
    spacing: float = 2.0,
    scale: Tuple[float, ...] = (3, 1, 1, 1, 1, 2, 1, 1, 1, 1),
    num_marks: int = 21,
    layer: LayerSpec = "WG",
) -> gf.Component:
    """Ruler structure for lithographic measurement.

    Args:
        height: Height of the ruling marks in um.
        width: Width of the ruling marks in um.
        spacing: Center-to-center spacing of the ruling marks in um.
        scale: Height scale pattern of marks.
        num_marks: Total number of marks to generate.
        layer: Specific layer to put the ruler geometry on.

    Notes:
        5.6.7: distribute across y instead of x.
    """
    D = gf.Component()
    for n in range(num_marks):
        h = height * scale[n % len(scale)]
        ref = D << gf.components.rectangle(size=(width, h), layer=layer)
        ref.rotate(90)

    D.distribute(direction="y", spacing=spacing, separation=False, edge="y")
    D.align(alignment="xmin")
    D.flatten()
    return D


c = litho_ruler(cache=False)
c
../_images/notebooks_08_pdk_46_0.png
[30]:
litho_ruler: uid 9b405880, ports [], references ['rectangle_1', 'rectangle_2', 'rectangle_3', 'rectangle_4', 'rectangle_5', 'rectangle_6', 'rectangle_7', 'rectangle_8', 'rectangle_9', 'rectangle_10', 'rectangle_11', 'rectangle_12', 'rectangle_13', 'rectangle_14', 'rectangle_15', 'rectangle_16', 'rectangle_17', 'rectangle_18', 'rectangle_19', 'rectangle_20', 'rectangle_21'], 0 polygons
[ ]: